﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Elentok.Common.Wpf.Helpers;

namespace Elentok.Common.Wpf.Controls
{
  /// <summary>
  /// Handles drag-and-drop in a <see cref="TreeView"/>
  /// </summary>
  /// <typeparam name="TItem">The data context type of each tree view item</typeparam>
  public class TreeViewDragDropManager<TItem>
  {
    private TreeView treeView;

    #region Drop
    private TreeViewItem lastDragOverItem;
    private bool lastCanDrop = false;
    private Action<TItem, IDataObject> drop;

    /// <summary>
    /// Gets or sets the handler that returns true if the dragged
    /// <see cref="IDataObject"/> can be dropped into an item
    /// </summary>
    public Func<TItem, IDataObject, bool> CanDrop { get; set; }

    /// <summary>
    /// Gets or sets the handler that drops <see cref="IDataObject"/>
    /// into an item
    /// </summary>
    public Action<TItem, IDataObject> Drop
    {
      get { return this.drop; }
      set
      {
        if (this.drop == value)
          return;

        var oldDrop = this.drop;

        this.drop = value;

        #region Connect/Disconnect from drop events
        if (oldDrop == null)
        {
          treeView.AllowDrop = true;
          treeView.PreviewDragOver += treeView_PreviewDragOver;
          treeView.PreviewDrop += treeView_PreviewDrop;
        }
        else if (this.drop == null)
        {
          treeView.AllowDrop = false;
          treeView.PreviewDragOver -= treeView_PreviewDragOver;
          treeView.PreviewDrop -= treeView_PreviewDrop;
        }
        #endregion
      }
    }

    private void treeView_PreviewDrop(object sender, DragEventArgs e)
    {
      if (this.lastDragOverItem != null)
      {
        TItem itemContext = (TItem)this.lastDragOverItem.DataContext;
        this.drop(itemContext, e.Data);
      }
    }

    private void treeView_PreviewDragOver(object sender, DragEventArgs e)
    {
      Point mousePosition = e.GetPosition(this.treeView);
      TreeViewItem item = this.treeView.GetTreeViewItemFromPoint(mousePosition);
      if (item == null)
      {
        e.Effects = DragDropEffects.None;
        this.lastCanDrop = false;
        this.lastDragOverItem = null;
      }
      else
      {
        if (item != lastDragOverItem)
        {
          TItem itemContext = (TItem)item.DataContext;
          if (this.CanDrop != null)
            this.lastCanDrop = this.CanDrop(itemContext, e.Data);
          else
            this.lastCanDrop = true;
          this.lastDragOverItem = item;
        }

        if (this.lastCanDrop)
          e.Effects = DragDropEffects.Move;
        else
          e.Effects = DragDropEffects.None;
      }

      e.Handled = true;
    }

    #endregion

    #region Drag

    private TreeViewItem draggedItem;
    private bool isDragging;
    private Point dragStartPoint;
    private Func<TItem, bool> canDrag;

    /// <summary>
    /// Gets or sets the handler that returns true
    /// if an item can be dragged
    /// </summary>
    public Func<TItem, bool> CanDrag
    {
      get { return this.canDrag; }
      set
      {
        if (this.canDrag == value)
          return;

        var oldCanDrag = this.canDrag;
        this.canDrag = value;

        if (oldCanDrag == null)
        {
          treeView.PreviewMouseLeftButtonDown += treeView_PreviewMouseLeftButtonDown;
          treeView.PreviewMouseMove += treeView_PreviewMouseMove;
        }
        else if (this.canDrag == null)
        {
          treeView.PreviewMouseLeftButtonDown -= treeView_PreviewMouseLeftButtonDown;
          treeView.PreviewMouseMove -= treeView_PreviewMouseMove;
        }
      }
    }

    private void treeView_PreviewMouseMove(object sender, MouseEventArgs e)
    {
      if (isDragging)
      {
        if (e.LeftButton != MouseButtonState.Pressed)
        {
          this.draggedItem = null;
          this.isDragging = false;
        }
        else
        {
          Point mousePos = e.GetPosition(null);
          Vector diff = dragStartPoint - mousePos;

          if (Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance &&
              Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance)
          {
            DataObject data = new DataObject(typeof(TItem), this.draggedItem.DataContext);
            DragDrop.DoDragDrop(this.treeView, data, DragDropEffects.Move);
          }
        }
      }
    }

    private void treeView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
      Point mousePosition = e.GetPosition(this.treeView);
      TreeViewItem item = this.treeView.GetTreeViewItemFromPoint(mousePosition);

      if (item == null)
        return;

      if (item != null)
      {
        this.draggedItem = item;
        this.isDragging = true;
        this.dragStartPoint = e.GetPosition(null);
      }
    }
    
    #endregion

    #region Ctor
    /// <summary>
    /// Initializes a new instance of the <see cref="TreeViewDragDropManager&lt;TItem&gt;"/> class.
    /// </summary>
    /// <param name="treeView"></param>
    /// <exception cref="ArgumentNullException"></exception>
    public TreeViewDragDropManager(TreeView treeView)
    {
      if (treeView == null)
        throw new ArgumentNullException("treeView");
      
      this.treeView = treeView;
    }
    #endregion
  }
}
